package util;

import com.crunch.core.Actor;
import com.crunch.math.*;

// a camera class
public class Camera extends Actor {
	public Camera() {
		super(100);
	}

	// view center
	private int vpWidth, vpHeight;
	private int vpX, vpY;
	private float x, y;
	private float zoom;
	private Matrix44f projectionMatrix = new Matrix44f();
	private Matrix33af viewMatrix = new Matrix33af();
	private Matrix33af cameraMatrix = new Matrix33af();
	private Matrix44f viewProjectionMatrix = new Matrix44f();

	public int getViewportWidth() {
		return vpWidth;
	}

	public int getViewportHeight() {
		return vpHeight;
	}

	public int getViewportX() {
		return vpX;
	}

	public int getViewportY() {
		return vpY;
	}

	public float getX() {
		return x;
	}

	public float getY() {
		return y;
	}

	public float getZoom() {
		return zoom;
	}

	public Matrix44f getProjectionMatrix() {
		return projectionMatrix;
	}

	// mutable for performance - don't modify
	public Matrix33af getViewMatrix() {
		return viewMatrix;
	}

	// mutable for performance - don't modify
	public Matrix33af getCameraMatrix() {
		return cameraMatrix;
	}

	// mutable for performance - don't modify
	public Matrix44f getViewProjectionMatrix() {
		return viewProjectionMatrix;
	}

	public void onCreate() {
		// call these to initialize region
		vpWidth = getEngine().window.getContentPane().getWidth();
		vpHeight = getEngine().window.getContentPane().getHeight();
		set(0.0f, 0.0f, 1.0f);
	}

	public void setViewport(int vpX, int vpY, int vpW, int vpH) {
		this.vpX = vpX;
		this.vpY = vpY;
		this.vpWidth = vpW;
		this.vpHeight = vpH;
		set(x, y, zoom);
	}

	public void set(float x, float y, float zoom) {
		this.x = x;
		this.y = y;
		this.zoom = zoom;

		float left = -0.5f * (float) vpWidth;
		float right = 0.5f * (float) vpWidth;
		float bottom = -0.5f * (float) vpHeight;
		float top = 0.5f * (float) vpHeight;
		float near = -1.0f;
		float far = 1.0f;

		projectionMatrix.set(
				2.0f/(right-left),  0.0f,               0.0f,               (right+left)/(left-right),
				0.0f,               2.0f/(top-bottom),  0.0f,               (top+bottom)/(bottom-top),
				0.0f,               0.0f,               2.0f/(near-far),    (far+near)/(near-far),
				0.0f,               0.0f,               0.0f,               1.0f);

		// camera matrix = t * s
		// view matrix = s^-1 * t^-1
		cameraMatrix.setIdentity();
		cameraMatrix.scaleAssign(new Vector2f(1.0f / this.zoom, 1.0f / this.zoom));
		cameraMatrix.translateAssign(new Vector2f(this.x, this.y));
		viewMatrix.setIdentity();
		viewMatrix.translateAssign(new Vector2f(-this.x, -this.y));
		viewMatrix.scaleAssign(new Vector2f(this.zoom, this.zoom));

		// vpMat = projMat * viewMat
		viewProjectionMatrix.set(projectionMatrix);
		viewProjectionMatrix.multiplyAssign(viewMatrix.getGraphicsMatrix());
	}

	// screen coordinates should be in range (-1, 1)
	public Vector2f screenToWorld(Vector2f p) {
		// convert screen coordinates
		Vector2f unProj = new Vector2f(
			p.x() * 0.5f * (float) vpWidth,
			p.y() * 0.5f * (float) vpHeight);

		// multiply by camera matrix
		return cameraMatrix.multiplyPoint(unProj);
	}

	// screen coords are in range (-1, 1)
	public Vector2f worldToScreen(Vector2f p) {
		Vector4f result = viewProjectionMatrix.multiply(new Vector4f(p.x(), p.y(), 0.0f, 1.0f));
		return new Vector2f(result.x(), result.y());
	}

	public Vector2f[] getViewBounds() {
		// could be rotated, take min/max of 4 points
		Vector2f pA = screenToWorld(new Vector2f(-1.0f, -1.0f));
		Vector2f pB = screenToWorld(new Vector2f(1.0f, -1.0f));
		Vector2f pC = screenToWorld(new Vector2f(-1.0f, 1.0f));
		Vector2f pD = screenToWorld(new Vector2f(1.0f, 1.0f));

		return new Vector2f[] {
				MathOps.min(MathOps.min(pA, pB), MathOps.min(pC, pD)),
				MathOps.max(MathOps.max(pA, pB), MathOps.max(pC, pD))
		};
	}
}
